﻿// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

#region #using Directives

using EF = System.Windows.Controls.DataVisualization.EnumerableFunctions;

#endregion

namespace System.Windows.Controls.DataVisualization.Charting
{
	#region #using Directives

	using System.Collections.Generic;

	using ComponentModel;

	using Diagnostics.CodeAnalysis;

	using Linq;

	using Shapes;

	#endregion

	internal class DateComparer : IEqualityComparer<DateTime>
	{
		#region IEqualityComparer<DateTime> Members

		public bool Equals(DateTime x, DateTime y)
		{
			return (x.Date == y.Date);
		}

		public int GetHashCode(DateTime obj)
		{
			return base.GetHashCode();
		}

		#endregion
	}

	/// <summary>
	/// An axis that displays numeric values.
	/// </summary>
	[StyleTypedProperty(Property = "GridLineStyle", StyleTargetType = typeof(Line))]
	[StyleTypedProperty(Property = "MajorTickMarkStyle", StyleTargetType = typeof(Line))]
	[StyleTypedProperty(Property = "MinorTickMarkStyle", StyleTargetType = typeof(Line))]
	[StyleTypedProperty(Property = "AxisLabelStyle", StyleTargetType = typeof(DateTimeAxisLabel))]
	[StyleTypedProperty(Property = "TitleStyle", StyleTargetType = typeof(Title))]
	[TemplatePart(Name = AxisGridName, Type = typeof(Grid))]
	[TemplatePart(Name = AxisTitleName, Type = typeof(Title))]
	public class CustomDateTimeAxis : RangeAxis
	{
		#region public List<DateTime> DisabledDates

		/// <summary>
		/// Identifies the DisabledDates dependency property.
		/// </summary>
		public static readonly DependencyProperty DisabledDatesProperty = DependencyProperty.Register("DisabledDates", typeof(List<DateTime>),
		                                                                                              typeof(CustomDateTimeAxis),
		                                                                                              new PropertyMetadata(new List<DateTime>()));

		/// <summary>
		/// Gets or sets the disabled dates for the chart.
		/// </summary>
		public List<DateTime> DisabledDates
		{
			get { return (List<DateTime>) GetValue(DisabledDatesProperty); }
			set { SetValue(DisabledDatesProperty, value); }
		}

		#endregion public List<DateTime> DisabledDates

		#region public List<DayOfWeek> DisabledDays

		/// <summary>
		/// Identifies the DisabledDays dependency property.
		/// </summary>
		public static readonly DependencyProperty DisabledDaysProperty = DependencyProperty.Register("DisabledDays", typeof(List<DayOfWeek>),
		                                                                                             typeof(CustomDateTimeAxis),
		                                                                                             new PropertyMetadata(new List<DayOfWeek>()));

		/// <summary>
		/// Gets or sets the disabled days for the chart.
		/// </summary>
		public List<DayOfWeek> DisabledDays
		{
			get { return (List<DayOfWeek>) GetValue(DisabledDaysProperty); }
			set { SetValue(DisabledDaysProperty, value); }
		}

		#endregion public List<DayOfWeek> DisabledDays

		#region public DateTime? ActualMaximum

		/// <summary>
		/// Identifies the ActualMaximum dependency property.
		/// </summary>
		public static readonly DependencyProperty ActualMaximumProperty = DependencyProperty.Register("ActualMaximum", typeof(DateTime?),
		                                                                                              typeof(CustomDateTimeAxis), null);

		/// <summary>
		/// Gets the actual maximum value plotted on the chart.
		/// </summary>
		public DateTime? ActualMaximum
		{
			get { return (DateTime?) GetValue(ActualMaximumProperty); }
			private set { SetValue(ActualMaximumProperty, value); }
		}

		#endregion public DateTime? ActualMaximum

		#region public DateTime? ActualMinimum

		/// <summary>
		/// Identifies the ActualMinimum dependency property.
		/// </summary>
		public static readonly DependencyProperty ActualMinimumProperty = DependencyProperty.Register("ActualMinimum", typeof(DateTime?),
		                                                                                              typeof(CustomDateTimeAxis), null);

		/// <summary>
		/// Gets the actual maximum value plotted on the chart.
		/// </summary>
		public DateTime? ActualMinimum
		{
			get { return (DateTime?) GetValue(ActualMinimumProperty); }
			private set { SetValue(ActualMinimumProperty, value); }
		}

		#endregion public DateTime? ActualMinimum

		#region public DateTime? Maximum

		/// <summary>
		/// Identifies the Maximum dependency property.
		/// </summary>
		public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(DateTime?), typeof(CustomDateTimeAxis),
		                                                                                        new PropertyMetadata(null, OnMaximumPropertyChanged));

		/// <summary>
		/// Gets or sets the maximum value plotted on the axis.
		/// </summary>
		[TypeConverter(typeof(NullableConverter<DateTime>))]
		public DateTime? Maximum
		{
			get { return (DateTime?) GetValue(MaximumProperty); }
			set { SetValue(MaximumProperty, value); }
		}

		/// <summary>
		/// MaximumProperty property changed handler.
		/// </summary>
		/// <param name="d">DateTimeAxis2 that changed its Maximum.</param>
		/// <param name="e">Event arguments.</param>
		private static void OnMaximumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var source = (CustomDateTimeAxis) d;
			var newValue = (DateTime?) e.NewValue;
			source.OnMaximumPropertyChanged(newValue);
		}

		/// <summary>
		/// MaximumProperty property changed handler.
		/// </summary>
		/// <param name="newValue">New value.</param>        
		private void OnMaximumPropertyChanged(DateTime? newValue)
		{
			ProtectedMaximum = newValue;
		}

		#endregion public DateTime? Maximum

		#region public DateTime? Minimum

		/// <summary>
		/// Identifies the Minimum dependency property.
		/// </summary>
		public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(DateTime?), typeof(CustomDateTimeAxis),
		                                                                                        new PropertyMetadata(null, OnMinimumPropertyChanged));

		/// <summary>
		/// Gets or sets the minimum value to plot on the axis.
		/// </summary>
		[TypeConverter(typeof(NullableConverter<DateTime>))]
		public DateTime? Minimum
		{
			get { return (DateTime?) GetValue(MinimumProperty); }
			set { SetValue(MinimumProperty, value); }
		}

		/// <summary>
		/// MinimumProperty property changed handler.
		/// </summary>
		/// <param name="d">DateTimeAxis2 that changed its Minimum.</param>
		/// <param name="e">Event arguments.</param>
		private static void OnMinimumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var source = (CustomDateTimeAxis) d;
			var newValue = (DateTime?) e.NewValue;
			source.OnMinimumPropertyChanged(newValue);
		}

		/// <summary>
		/// MinimumProperty property changed handler.
		/// </summary>
		/// <param name="newValue">New value.</param>        
		private void OnMinimumPropertyChanged(DateTime? newValue)
		{
			ProtectedMinimum = newValue;
		}

		#endregion public DateTime? Minimum

		#region public double? Interval

		/// <summary>
		/// Identifies the Interval dependency property.
		/// </summary>
		public static readonly DependencyProperty IntervalProperty = DependencyProperty.Register("Interval", typeof(double?), typeof(CustomDateTimeAxis),
		                                                                                         new PropertyMetadata(null, OnIntervalPropertyChanged));

		/// <summary>
		/// Gets or sets the axis interval.
		/// </summary>
		[TypeConverter(typeof(NullableConverter<double>))]
		public double? Interval
		{
			get { return (double?) GetValue(IntervalProperty); }
			set { SetValue(IntervalProperty, value); }
		}

		/// <summary>
		/// IntervalProperty property changed handler.
		/// </summary>
		/// <param name="d">DateTimeAxis2 that changed its Interval.</param>
		/// <param name="e">Event arguments.</param>
		private static void OnIntervalPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var source = (CustomDateTimeAxis) d;
			source.OnIntervalPropertyChanged();
		}

		/// <summary>
		/// IntervalProperty property changed handler.
		/// </summary>
		private void OnIntervalPropertyChanged()
		{
			Invalidate();
		}

		#endregion public double? Interval

		#region public double ActualInterval

		/// <summary>
		/// Identifies the ActualInterval dependency property.
		/// </summary>
		public static readonly DependencyProperty ActualIntervalProperty = DependencyProperty.Register("ActualInterval", typeof(double),
		                                                                                               typeof(CustomDateTimeAxis),
		                                                                                               new PropertyMetadata(double.NaN));

		/// <summary>
		/// Gets the actual interval.
		/// </summary>
		public double ActualInterval
		{
			get { return (double) GetValue(ActualIntervalProperty); }
			private set { SetValue(ActualIntervalProperty, value); }
		}

		#endregion public double ActualInterval

		#region public DateTimeIntervalType IntervalType

		/// <summary>
		/// Identifies the InternalIntervalType dependency property.
		/// </summary>
		public static readonly DependencyProperty IntervalTypeProperty = DependencyProperty.Register("IntervalType", typeof(DateTimeIntervalType),
		                                                                                             typeof(CustomDateTimeAxis),
		                                                                                             new PropertyMetadata(DateTimeIntervalType.Auto,
		                                                                                                                  OnIntervalTypePropertyChanged));

		/// <summary>
		/// Gets or sets the interval to use for the axis.
		/// </summary>
		public DateTimeIntervalType IntervalType
		{
			get { return (DateTimeIntervalType) GetValue(IntervalTypeProperty); }
			set { SetValue(IntervalTypeProperty, value); }
		}

		/// <summary>
		/// IntervalTypeProperty property changed handler.
		/// </summary>
		/// <param name="d">DateTimeAxis that changed its InternalIntervalType.</param>
		/// <param name="e">Event arguments.</param>
		private static void OnIntervalTypePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var source = (CustomDateTimeAxis) d;
			var newValue = (DateTimeIntervalType) e.NewValue;
			source.OnIntervalTypePropertyChanged(newValue);
		}

		/// <summary>
		/// IntervalTypeProperty property changed handler.
		/// </summary>
		/// <param name="newValue">New value.</param>        
		private void OnIntervalTypePropertyChanged(DateTimeIntervalType newValue)
		{
			ActualIntervalType = newValue;
			Invalidate();
		}

		#endregion public DateTimeIntervalType IntervalType

		#region public DateTimeIntervalType ActualIntervalType

		/// <summary>
		/// Identifies the ActualIntervalType dependency property.
		/// </summary>
		private static readonly DependencyProperty ActualIntervalTypeProperty = DependencyProperty.Register("ActualIntervalType",
		                                                                                                    typeof(DateTimeIntervalType),
		                                                                                                    typeof(CustomDateTimeAxis),
		                                                                                                    new PropertyMetadata(DateTimeIntervalType.Auto));

		/// <summary>
		/// Gets or sets the actual interval type.
		/// </summary>
		private DateTimeIntervalType ActualIntervalType
		{
			get { return (DateTimeIntervalType) GetValue(ActualIntervalTypeProperty); }
			set { SetValue(ActualIntervalTypeProperty, value); }
		}

		#endregion public DateTimeIntervalType ActualIntervalType

		private readonly DateComparer dateComparer = new DateComparer();

		/// <summary>
		/// Instantiates a new instance of the DateTimeAxis2 class.
		/// </summary>
		public CustomDateTimeAxis()
		{
			int year = DateTime.Now.Year;
			ActualRange = new Range<IComparable>(new DateTime(year, 1, 1), new DateTime(year + 1, 1, 1));
		}

		/// <summary>
		/// Gets the origin value on the axis.
		/// </summary>
		protected override IComparable Origin
		{
			get { return null; }
		}

		/// <summary>
		/// Converts a range to a range of type DateTime.
		/// </summary>
		/// <param name="range">A range to be converted.</param>
		/// <returns>A range that has been converted.</returns>
		private static Range<DateTime> ToDateTimeRange(Range<IComparable> range)
		{
			if (!range.HasData)
			{
				return new Range<DateTime>();
			}
			else
			{
				return new Range<DateTime>(ValueHelper.ToDateTime(range.Minimum), ValueHelper.ToDateTime(range.Maximum));
			}
		}

		/// <summary>
		/// Creates a new instance of the DateTimeAxisLabel class.
		/// </summary>
		/// <returns>Returns  a new instance of the DateTimeAxisLabel class.
		/// </returns>
		protected override Control CreateAxisLabel()
		{
			return new DateTimeAxisLabel();
		}

		/// <summary>
		/// Prepares an instance of the DateTimeAxisLabel class by setting its
		/// IntervalType property.
		/// </summary>
		/// <param name="label">An instance of the DateTimeAxisLabel class.
		/// </param>
		/// <param name="dataContext">The data context to assign to the label.
		/// </param>
		protected override void PrepareAxisLabel(Control label, object dataContext)
		{
			var dateTimeAxisLabel = label as DateTimeAxisLabel;

			if (dateTimeAxisLabel != null)
			{
				dateTimeAxisLabel.IntervalType = ActualIntervalType;
			}
			base.PrepareAxisLabel(label, dataContext);
		}

		/// <summary>
		/// Updates the typed actual maximum and minimum properties when the
		/// actual range changes.
		/// </summary>
		/// <param name="range">The actual range.</param>
		protected override void OnActualRangeChanged(Range<IComparable> range)
		{
			if (range.HasData)
			{
				ActualMaximum = (DateTime) range.Maximum;
				ActualMinimum = (DateTime) range.Minimum;
			}
			else
			{
				ActualMaximum = null;
				ActualMinimum = null;
			}

			base.OnActualRangeChanged(range);
		}

		/// <summary>
		/// Returns a value indicating whether a value can plot.
		/// </summary>
		/// <param name="value">The value to plot.</param>
		/// <returns>A value indicating whether a value can plot.</returns>
		public override bool CanPlot(object value)
		{
			DateTime val;
			if (ValueHelper.TryConvert(value, out val))
			{
				return !IsDateDisabled(val);
			}

			return false;
		}

		private bool IsDateDisabled(DateTime date)
		{
			return DisabledDays.Contains(date.DayOfWeek) || DisabledDates.Contains(date, this.dateComparer);
		}

		/// <summary>
		/// Gets the disabled days in range.
		/// </summary>
		/// <param name="range">The range.</param>
		/// <returns></returns>
		protected int GetDisabledDaysInRange(Range<DateTime> range)
		{
			int numDisabledDays = 0;
			var disabledDays = DisabledDays;
			var disabledDates = DisabledDates;
			if (disabledDates.Any() || disabledDays.Any())
			{
				for (var d = range.Minimum; d <= range.Maximum; d = d.AddDays(1.0))
				{
					if (disabledDays.Contains(d.DayOfWeek) || disabledDates.Contains(d, this.dateComparer))
					{
						numDisabledDays++;
					}
				}
			}

			return numDisabledDays;
		}

		/// <summary>
		/// Returns the plot area coordinate of a value.
		/// </summary>
		/// <param name="value">The value to plot.</param>
		/// <param name="currentRange">The range to use determine the coordinate.</param>
		/// <param name="length">The length of the axis.</param>
		/// <returns>The plot area coordinate of a value.</returns>
		protected override UnitValue GetPlotAreaCoordinate(object value, Range<IComparable> currentRange, double length)
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}

			if (currentRange.HasData)
			{
				var dateTimeValue = ValueHelper.ToDateTime(value);

				var actualDateTimeRange = ToDateTimeRange(currentRange);

				if (IsDateDisabled(dateTimeValue) || dateTimeValue < actualDateTimeRange.Minimum)
				{
					return UnitValue.NaN();
				}

				double rangelength = actualDateTimeRange.Maximum.ToOADate() - actualDateTimeRange.Minimum.ToOADate() - GetDisabledDaysInRange(actualDateTimeRange);
				double pixelLength = Math.Max(length - 1, 0);

				return
					new UnitValue(
						(dateTimeValue.ToOADate() - actualDateTimeRange.Minimum.ToOADate() -
						 GetDisabledDaysInRange(new Range<DateTime>(actualDateTimeRange.Minimum, dateTimeValue))) * (pixelLength / rangelength), Unit.Pixels);
			}

			return UnitValue.NaN();
		}

		/// <summary>
		/// Returns the actual interval to use to determine which values are 
		/// displayed in the axis.
		/// </summary>
		/// <param name="availableSize">The available size.</param>
		/// <returns>The actual interval to use to determine which values are 
		/// displayed in the axis.
		/// </returns>
		private double CalculateActualInterval(Size availableSize)
		{
			if (Interval != null)
			{
				return Interval.Value;
			}

			var actualDateTimeRange = ToDateTimeRange(ActualRange);

			DateTimeIntervalType intervalType;
			double interval = CalculateDateTimeInterval(actualDateTimeRange.Minimum, actualDateTimeRange.Maximum, out intervalType, availableSize);
			ActualIntervalType = intervalType;
			return interval;
		}

		/// <summary>
		/// Returns a sequence of major values.
		/// </summary>
		/// <param name="availableSize">The available size.</param>
		/// <returns>A sequence of major values.</returns>
		protected virtual IEnumerable<DateTime> GetMajorAxisValues(Size availableSize)
		{
			if (!ActualRange.HasData || ValueHelper.Compare(ActualRange.Minimum, ActualRange.Maximum) == 0 || GetLength(availableSize) == 0.0)
			{
				yield break;
			}

			ActualInterval = CalculateActualInterval(availableSize);
			var dateTimeRange = ToDateTimeRange(ActualRange);
			var date = dateTimeRange.Minimum;

			var start = AlignIntervalStart(dateTimeRange.Minimum, ActualInterval, ActualIntervalType);
			while (start < dateTimeRange.Minimum)
			{
				start = IncrementDateTime(start, ActualInterval);
			}

			var intermediateDates =
				EnumerableFunctions.Iterate(start, next => IncrementDateTime(next, ActualInterval)).TakeWhile(current => dateTimeRange.Contains(current));

			var disabledDays = DisabledDays;
			var disabledDates = DisabledDates;
			foreach (var current in intermediateDates)
			{
				if (disabledDays.Contains(current.DayOfWeek) || disabledDates.Contains(current, this.dateComparer))
				{
					continue;
				}

				yield return current;
			}
		}

		/// <summary>
		/// Returns a sequence of values to create major tick marks for.
		/// </summary>
		/// <param name="availableSize">The available size.</param>
		/// <returns>A sequence of values to create major tick marks for.
		/// </returns>
		protected override IEnumerable<IComparable> GetMajorTickMarkValues(Size availableSize)
		{
			return GetMajorAxisValues(availableSize).Cast<IComparable>();
		}

		/// <summary>
		/// Returns a sequence of values to plot on the axis.
		/// </summary>
		/// <param name="availableSize">The available size.</param>
		/// <returns>A sequence of values to plot on the axis.</returns>
		protected override IEnumerable<IComparable> GetLabelValues(Size availableSize)
		{
			return GetMajorAxisValues(availableSize).Cast<IComparable>();
		}

		/// <summary>
		/// This method accepts a date time and increments it.
		/// </summary>
		/// <param name="date">A date time.</param>
		/// <param name="interval">The interval used to increment the date time.
		/// </param>
		/// <returns>The new date time.</returns>
		private DateTime IncrementDateTime(DateTime date, double interval)
		{
			var intervalType = ActualIntervalType;
			var span = new TimeSpan(0);
			DateTime result;

			if (intervalType == DateTimeIntervalType.Days)
			{
				span = TimeSpan.FromDays(interval);
			}
			else if (intervalType == DateTimeIntervalType.Hours)
			{
				span = TimeSpan.FromHours(interval);
			}
			else if (intervalType == DateTimeIntervalType.Milliseconds)
			{
				span = TimeSpan.FromMilliseconds(interval);
			}
			else if (intervalType == DateTimeIntervalType.Seconds)
			{
				span = TimeSpan.FromSeconds(interval);
			}
			else if (intervalType == DateTimeIntervalType.Minutes)
			{
				span = TimeSpan.FromMinutes(interval);
			}
			else if (intervalType == DateTimeIntervalType.Weeks)
			{
				span = TimeSpan.FromDays(7.0 * interval);
			}
			else if (intervalType == DateTimeIntervalType.Months)
			{
				// Special case handling when current date point
				// to the last day of the month
				bool lastMonthDay = false;
				if (date.Day == DateTime.DaysInMonth(date.Year, date.Month))
				{
					lastMonthDay = true;
				}

				// Add specified amount of months
				date = date.AddMonths((int) Math.Floor(interval));
				span = TimeSpan.FromDays(30.0 * (interval - Math.Floor(interval)));

				// Check if last month of the day was used
				if (lastMonthDay && span.Ticks == 0)
				{
					// Make sure the last day of the month is selected
					int daysInMobth = DateTime.DaysInMonth(date.Year, date.Month);
					date = date.AddDays(daysInMobth - date.Day);
				}
			}
			else if (intervalType == DateTimeIntervalType.Years)
			{
				date = date.AddYears((int) Math.Floor(interval));
				span = TimeSpan.FromDays(365.0 * (interval - Math.Floor(interval)));
			}

			result = date.Add(span);

			return result;
		}

		/// <summary>
		/// Adjusts the beginning of the first interval depending on the type and size.
		/// </summary>
		/// <param name="start">Original start point.</param>
		/// <param name="intervalSize">Interval size.</param>
		/// <param name="type">Type of the interval (Month, Year, ...).</param>
		/// <returns>
		/// Adjusted interval start position.
		/// </returns>
		private static DateTime AlignIntervalStart(DateTime start, double intervalSize, DateTimeIntervalType type)
		{
			// Do not adjust start position for these interval type
			if (type == DateTimeIntervalType.Auto)
			{
				return start;
			}

			// Get the beginning of the interval depending on type
			var newStartDate = start;

			// Adjust the months interval depending on size
			if (intervalSize > 0.0 && intervalSize != 1.0)
			{
				if (type == DateTimeIntervalType.Months && intervalSize <= 12.0 && intervalSize > 1)
				{
					// Make sure that the beginning is aligned correctly for cases
					// like quarters and half years
					var resultDate = newStartDate;
					var sizeAdjustedDate = new DateTime(newStartDate.Year, 1, 1, 0, 0, 0);
					while (sizeAdjustedDate < newStartDate)
					{
						resultDate = sizeAdjustedDate;
						sizeAdjustedDate = sizeAdjustedDate.AddMonths((int) intervalSize);
					}

					newStartDate = resultDate;
					return newStartDate;
				}
			}

			// Check interval type
			switch (type)
			{
				case DateTimeIntervalType.Years:
					int year = (int) ((int) (newStartDate.Year / intervalSize) * intervalSize);
					if (year <= 0)
					{
						year = 1;
					}
					newStartDate = new DateTime(year, 1, 1, 0, 0, 0);
					break;

				case DateTimeIntervalType.Months:
					int month = (int) ((int) (newStartDate.Month / intervalSize) * intervalSize);
					if (month <= 0)
					{
						month = 1;
					}
					newStartDate = new DateTime(newStartDate.Year, month, 1, 0, 0, 0);
					break;

				case DateTimeIntervalType.Days:
					int day = (int) ((int) (newStartDate.Day / intervalSize) * intervalSize);
					if (day <= 0)
					{
						day = 1;
					}
					newStartDate = new DateTime(newStartDate.Year, newStartDate.Month, day, 0, 0, 0);
					break;

				case DateTimeIntervalType.Hours:
					int hour = (int) ((int) (newStartDate.Hour / intervalSize) * intervalSize);
					newStartDate = new DateTime(newStartDate.Year, newStartDate.Month, newStartDate.Day, hour, 0, 0);
					break;

				case DateTimeIntervalType.Minutes:
					int minute = (int) ((int) (newStartDate.Minute / intervalSize) * intervalSize);
					newStartDate = new DateTime(newStartDate.Year, newStartDate.Month, newStartDate.Day, newStartDate.Hour, minute, 0);
					break;

				case DateTimeIntervalType.Seconds:
					int second = (int) ((int) (newStartDate.Second / intervalSize) * intervalSize);
					newStartDate = new DateTime(newStartDate.Year, newStartDate.Month, newStartDate.Day, newStartDate.Hour, newStartDate.Minute, second, 0);
					break;

				case DateTimeIntervalType.Milliseconds:
					int milliseconds = (int) ((int) (newStartDate.Millisecond / intervalSize) * intervalSize);
					newStartDate = new DateTime(newStartDate.Year, newStartDate.Month, newStartDate.Day, newStartDate.Hour, newStartDate.Minute, newStartDate.Second,
					                            milliseconds);
					break;

				case DateTimeIntervalType.Weeks:

					// Elements that have interval set to weeks should be aligned to the 
					// nearest start of week no matter how many weeks is the interval.
					newStartDate = new DateTime(newStartDate.Year, newStartDate.Month, newStartDate.Day, 0, 0, 0);

					newStartDate = newStartDate.AddDays(-((int) newStartDate.DayOfWeek));
					break;
			}

			return newStartDate;
		}

		/// <summary>
		/// Returns the value range given a plot area coordinate.
		/// </summary>
		/// <param name="value">The position.</param>
		/// <returns>A range of values at that plot area coordinate.</returns>
		protected override IComparable GetValueAtPosition(UnitValue value)
		{
			if (ActualRange.HasData && ActualLength != 0.0)
			{
				double coordinate = value.Value;
				if (value.Unit == Unit.Pixels)
				{
					var actualDateTimeRange = ToDateTimeRange(ActualRange);

					double minimumAsDouble = actualDateTimeRange.Minimum.ToOADate();
					double rangelength = actualDateTimeRange.Maximum.ToOADate() - minimumAsDouble;
					var output = DateTime.FromOADate((coordinate * (rangelength / ActualLength)) + minimumAsDouble);

					return output;
				}
				else
				{
					throw new NotImplementedException();
				}
			}

			return null;
		}

		/// <summary>
		/// Recalculates a DateTime interval obtained from maximum and minimum.
		/// </summary>
		/// <param name="minimum">The minimum.</param>
		/// <param name="maximum">The maximum.</param>
		/// <param name="type">Date time interval type.</param>
		/// <param name="availableSize">The available size.</param>
		/// <returns>Auto Interval.</returns>
		[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity",
			Justification =
				"The method should inspect all variations of time span (millisec to year) and contains long case. Otherwise is simple and readable.")]
		private double CalculateDateTimeInterval(DateTime minimum, DateTime maximum, out DateTimeIntervalType type, Size availableSize)
		{
			var dateTimeMin = minimum;
			var dateTimeMax = maximum;
			var timeSpan = dateTimeMax.Subtract(dateTimeMin);

			// this algorithm is designed to return close to 10 intervals.
			// we need to align the time span for PrefferedNumberOfIntervals
			double maxIntervals = Orientation == AxisOrientation.X ? MaximumAxisIntervalsPer200Pixels * 0.8 : MaximumAxisIntervalsPer200Pixels;
			double rangeMultiplicator = GetLength(availableSize) / (200 * 10 / maxIntervals);
			timeSpan = new TimeSpan((long) ((double) timeSpan.Ticks / rangeMultiplicator));

			// Minutes
			double inter = timeSpan.TotalMinutes;

			// For Range less than 60 seconds interval is 5 sec
			if (inter <= 1.0)
			{
				// Milli Seconds
				double milliSeconds = timeSpan.TotalMilliseconds;
				if (milliSeconds <= 10)
				{
					type = DateTimeIntervalType.Milliseconds;
					return 1;
				}
				if (milliSeconds <= 50)
				{
					type = DateTimeIntervalType.Milliseconds;
					return 4;
				}
				if (milliSeconds <= 200)
				{
					type = DateTimeIntervalType.Milliseconds;
					return 20;
				}
				if (milliSeconds <= 500)
				{
					type = DateTimeIntervalType.Milliseconds;
					return 50;
				}

				// Seconds
				double seconds = timeSpan.TotalSeconds;

				if (seconds <= 7)
				{
					type = DateTimeIntervalType.Seconds;
					return 1;
				}
				else if (seconds <= 15)
				{
					type = DateTimeIntervalType.Seconds;
					return 2;
				}
				else if (seconds <= 30)
				{
					type = DateTimeIntervalType.Seconds;
					return 5;
				}
				else if (seconds <= 60)
				{
					type = DateTimeIntervalType.Seconds;
					return 10;
				}
			}
			else if (inter <= 2.0)
			{
				// For Range less than 120 seconds interval is 10 sec
				type = DateTimeIntervalType.Seconds;
				return 20;
			}
			else if (inter <= 3.0)
			{
				// For Range less than 180 seconds interval is 30 sec
				type = DateTimeIntervalType.Seconds;
				return 30;
			}
			else if (inter <= 10)
			{
				// For Range less than 10 minutes interval is 1 min
				type = DateTimeIntervalType.Minutes;
				return 1;
			}
			else if (inter <= 20)
			{
				// For Range less than 20 minutes interval is 1 min
				type = DateTimeIntervalType.Minutes;
				return 2;
			}
			else if (inter <= 60)
			{
				// For Range less than 60 minutes interval is 5 min
				type = DateTimeIntervalType.Minutes;
				return 5;
			}
			else if (inter <= 120)
			{
				// For Range less than 120 minutes interval is 10 min
				type = DateTimeIntervalType.Minutes;
				return 10;
			}
			else if (inter <= 180)
			{
				// For Range less than 180 minutes interval is 30 min
				type = DateTimeIntervalType.Minutes;
				return 30;
			}
			else if (inter <= 60 * 12)
			{
				// For Range less than 12 hours interval is 1 hour
				type = DateTimeIntervalType.Hours;
				return 1;
			}
			else if (inter <= 60 * 24)
			{
				// For Range less than 24 hours interval is 4 hour
				type = DateTimeIntervalType.Hours;
				return 4;
			}
			else if (inter <= 60 * 24 * 2)
			{
				// For Range less than 2 days interval is 6 hour
				type = DateTimeIntervalType.Hours;
				return 6;
			}
			else if (inter <= 60 * 24 * 3)
			{
				// For Range less than 3 days interval is 12 hour
				type = DateTimeIntervalType.Hours;
				return 12;
			}
			else if (inter <= 60 * 24 * 10)
			{
				// For Range less than 10 days interval is 1 day
				type = DateTimeIntervalType.Days;
				return 1;
			}
			else if (inter <= 60 * 24 * 20)
			{
				// For Range less than 20 days interval is 2 day
				type = DateTimeIntervalType.Days;
				return 2;
			}
			else if (inter <= 60 * 24 * 30)
			{
				// For Range less than 30 days interval is 3 day
				type = DateTimeIntervalType.Days;
				return 3;
			}
			else if (inter <= 60 * 24 * 30.5 * 2)
			{
				// For Range less than 2 months interval is 1 week
				type = DateTimeIntervalType.Weeks;
				return 1;
			}
			else if (inter <= 60 * 24 * 30.5 * 5)
			{
				// For Range less than 5 months interval is 2weeks
				type = DateTimeIntervalType.Weeks;
				return 2;
			}
			else if (inter <= 60 * 24 * 30.5 * 12)
			{
				// For Range less than 12 months interval is 1 month
				type = DateTimeIntervalType.Months;
				return 1;
			}
			else if (inter <= 60 * 24 * 30.5 * 24)
			{
				// For Range less than 24 months interval is 3 month
				type = DateTimeIntervalType.Months;
				return 3;
			}
			else if (inter <= 60 * 24 * 30.5 * 48)
			{
				// For Range less than 48 months interval is 6 months 
				type = DateTimeIntervalType.Months;
				return 6;
			}

			// For Range more than 48 months interval is year 
			type = DateTimeIntervalType.Years;
			double years = inter / 60 / 24 / 365;
			if (years < 5)
			{
				return 1;
			}
			else if (years < 10)
			{
				return 2;
			}

			// Make a correction of the interval
			return Math.Floor(years / 5);
		}

		/// <summary>
		/// Overrides the actual range to ensure that it is never set to an
		/// empty range.
		/// </summary>
		/// <param name="range">The range to override.</param>
		/// <returns>The overridden range.</returns>
		[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity",
			Justification = "This method is very difficult to break up cleanly.")]
		protected override Range<IComparable> OverrideDataRange(Range<IComparable> range)
		{
			var overriddenActualRange = range;

			if (!overriddenActualRange.HasData)
			{
				var date = DateTime.Now.Date;
				return new Range<IComparable>(date, date.AddDays(1.0));
			}
			if (range.HasData && ActualLength != 0.0)
			{
				IList<ValueMarginCoordinateAndOverlap> valueMargins = new List<ValueMarginCoordinateAndOverlap>();
				foreach (var valueMargin in
					RegisteredListeners.OfType<IValueMarginProvider>().SelectMany(provider => provider.GetValueMargins(this)))
				{
					valueMargins.Add(new ValueMarginCoordinateAndOverlap
					                 	{
					                 		ValueMargin = valueMargin,
					                 	});
				}

				if (valueMargins.Count > 0)
				{
					double maximumPixelMarginLength =
						valueMargins.Select(valueMargin => valueMargin.ValueMargin.LowMargin + valueMargin.ValueMargin.HighMargin).MaxOrNullable().Value;

					// Requested margin is larger than the axis so give up
					// trying to find a range that will fit it.
					if (maximumPixelMarginLength > ActualLength)
					{
						return range;
					}

					var currentRange = range.ToDateTimeRange();

					// Ensure range is not empty.
					if (currentRange.Minimum == currentRange.Maximum)
					{
						var date = DateTime.Now.Date;
						currentRange = new Range<DateTime>(date, date.AddDays(1.0));
					}

					// priming the loop
					double actualLength = ActualLength;
					ValueMarginCoordinateAndOverlap maxLeftOverlapValueMargin;
					ValueMarginCoordinateAndOverlap maxRightOverlapValueMargin;
					UpdateValueMargins(valueMargins, currentRange.ToComparableRange());
					GetMaxLeftAndRightOverlap(valueMargins, out maxLeftOverlapValueMargin, out maxRightOverlapValueMargin);

					while (maxLeftOverlapValueMargin.LeftOverlap > 0 || maxRightOverlapValueMargin.RightOverlap > 0)
					{
						long unitOverPixels = currentRange.GetLength().Value.Ticks / ((long) actualLength);
						var newMinimum = new DateTime(currentRange.Minimum.Ticks - (long) ((maxLeftOverlapValueMargin.LeftOverlap + 0.5) * unitOverPixels));
						var newMaximum = new DateTime(currentRange.Maximum.Ticks + (long) ((maxRightOverlapValueMargin.RightOverlap + 0.5) * unitOverPixels));

						currentRange = new Range<DateTime>(newMinimum, newMaximum);
						UpdateValueMargins(valueMargins, currentRange.ToComparableRange());
						GetMaxLeftAndRightOverlap(valueMargins, out maxLeftOverlapValueMargin, out maxRightOverlapValueMargin);
					}

					return currentRange.ToComparableRange();
				}
			}

			return range;
		}

		/// <summary>
		/// Create function that when given a range will return the 
		/// amount in pixels by which the value margin range 
		/// overlaps.  Positive numbers represent values outside the
		/// range.
		/// </summary>
		/// <param name="valueMargins">The list of value margins, coordinates, and overlaps.</param>
		/// <param name="comparableRange">The new range to use to calculate coordinates.</param>
		internal void UpdateValueMargins(IList<ValueMarginCoordinateAndOverlap> valueMargins, Range<IComparable> comparableRange)
		{
			double actualLength = ActualLength;
			int valueMarginsCount = valueMargins.Count;
			for (int count = 0; count < valueMarginsCount; count++)
			{
				var item = valueMargins[count];
				item.Coordinate = GetPlotAreaCoordinate(item.ValueMargin.Value, comparableRange, actualLength).Value;
				item.LeftOverlap = -(item.Coordinate - item.ValueMargin.LowMargin);
				item.RightOverlap = (item.Coordinate + item.ValueMargin.HighMargin) - actualLength;
			}
		}

		/// <summary>
		/// Returns the value margin, coordinate, and overlap triples that have the largest left and right overlap.
		/// </summary>
		/// <param name="valueMargins">The list of value margin, coordinate, and 
		/// overlap triples.</param>
		/// <param name="maxLeftOverlapValueMargin">The value margin, 
		/// coordinate, and overlap triple that has the largest left overlap.
		/// </param>
		/// <param name="maxRightOverlapValueMargin">The value margin, 
		/// coordinate, and overlap triple that has the largest right overlap.
		/// </param>
		internal static void GetMaxLeftAndRightOverlap(IList<ValueMarginCoordinateAndOverlap> valueMargins,
		                                               out ValueMarginCoordinateAndOverlap maxLeftOverlapValueMargin,
		                                               out ValueMarginCoordinateAndOverlap maxRightOverlapValueMargin)
		{
			maxLeftOverlapValueMargin = null;
			maxRightOverlapValueMargin = null;
			double maxLeftOverlap = double.MinValue;
			double maxRightOverlap = double.MinValue;
			int valueMarginsCount = valueMargins.Count;
			for (int cnt = 0; cnt < valueMarginsCount; cnt++)
			{
				var valueMargin = valueMargins[cnt];
				double leftOverlap = valueMargin.LeftOverlap;
				if (leftOverlap > maxLeftOverlap)
				{
					maxLeftOverlap = leftOverlap;
					maxLeftOverlapValueMargin = valueMargin;
				}
				double rightOverlap = valueMargin.RightOverlap;
				if (rightOverlap > maxRightOverlap)
				{
					maxRightOverlap = rightOverlap;
					maxRightOverlapValueMargin = valueMargin;
				}
			}
		}
	}
}